/*
 * main.c
 *
 *  Created on: 30/04/2011
 *      Author: Yotam and Gilaad
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>

#include "rentals.h"
#include "mtm_ex2.h"

//**********************************************************************
//**                             Defines                              **
//**********************************************************************

#define PARAMETER_INPUT_PREFIX "-i"
#define PARAMETER_OUTPUT_PREFIX "-o"

#define LOAD_SUCCESS 0
#define LOAD_FAIL 1

#define FILE_BAD_PREFIX 0
#define FILE_NONE 1
#define FILE_IN 2
#define FILE_OUT 3

#define READ_MODE "r"
#define WRITE_MODE "w"

#define ZERO 0

// binary values for validation methods
#define TRUE 0
#define FALSE 1

// the longest command holds 6 parameters, if we reach 7 then the command is
// invalid
#define MAX_WORDS_IN_COMMAND 7
#define MAX_LINE_LENGTH 120

// the commands defined by the protocol
#define COMMAND_CAR "car"
#define COMMAND_CUSTOMER "customer"
#define COMMAND_REPORT "report"
#define COMMAND_COMMENT "#"

// the commands defined by the protocol
#define SUBCOMMAND_ADD "add"
#define SUBCOMMAND_REMOVE "remove"
#define SUBCOMMAND_SALE "sale"
#define SUBCOMMAND_NO_SALE "nosale"
#define SUBCOMMAND_NEW "new"
#define SUBCOMMAND_RENT "rent"
#define SUBCOMMAND_RETURN "return"
#define SUBCOMMAND_PAY "pay"
#define SUBCOMMAND_AVAILABLE_CARS "available_cars"
#define SUBCOMMAND_BILLS "bills"
#define SUBCOMMAND_CUSTOMER "customer"
#define SUBCOMMAND_MONTHLY "monthly"

/*
 * This is the input handler that parses the incoming commands and validates
 * the input. For valid input the manager functions are invoked.
 */
typedef struct {
	Rentals rentals;// This is the manager that holds all rentals information.
	FILE *input;// This is the input stream
	FILE *output;// This is the output stream
	FILE *error;// This is the error stream
}InputHandler;

/*!
 * In order to use strtok we need to define the delimiters between two words
 * The first character is space.
 * \t is a tab character.
 * \r & \n mark newlines, and their appearance is different on different
 * operating systems. So in Linux environments a new line appears as \n while
 * in a Windows environment a newline is \r\n.
 *
 * By using all of these 4 tokens strtok will work in all cases for us.
 */
#define PARAMETER_DELIMITER " \t\r\n"


/**************************************************************************/
/*!
 * This function takes a string and splits it to seperate words that will be
 * placed in the supplied array
 *
 * Note the usage of strtok - the first call is with the line to parse.
 * Each subsequent call is with the first parameter being NULL to signal strtok
 * to continue working on the previous string.
 * strtok changes the string you supply it, so we copy all strings to avoid
 * problems.
 * In the general case strtok is a bad function, because it changes its input
 * and uses static variables. But for this exercise it's fine to use.
 *
 * @param line - the line that will be parsed to words
 * @param paremeters - array of buffers to store the parsed words in.
 * @return
 */
int splitLineIntoWords(char* line, char* parameters[]){
	int i;
	char *current = line;
	int end;
	//fill the parameters array with the delimited tokens
	for (i=0; i < MAX_WORDS_IN_COMMAND; ++i){
		//get the next token from the string
		parameters[i] = strtok(current, PARAMETER_DELIMITER);
		if (parameters[i] == NULL){
			return i;
		}
		current = NULL;

		//null terminate the current string at the closest delimiter
		end = strcspn(parameters[i], PARAMETER_DELIMITER);
		if (end < strlen(parameters[i])){
			parameters[i][end] = '\0';
		}
	}
	//return the amount of words we read
	return i;
}

/*
 * prints the given return code to the given file stream.
 * @param stream the stream to print to
 * @param result the return code to print
 */
void printReturnCode(FILE *stream, RentalsResult result) {
	switch (result) {
	case RENTALS_OUT_OF_MEMORY:
		mtmPrintErrorMessage(stream,MTM_OUT_OF_MEMORY);
		break;
	case RENTALS_CANNOT_OPEN_FILE:
		mtmPrintErrorMessage(stream,MTM_CANNOT_OPEN_FILE);
		break;
	case RENTALS_INVALID_COMMAND_LINE_PARAMETERS:
		mtmPrintErrorMessage(stream, MTM_INVALID_COMMAND_LINE_PARAMETERS);
	    break;
	case RENTALS_CAR_ALREADY_EXISTS:
		mtmPrintErrorMessage(stream,MTM_CAR_ALREADY_EXISTS);
		break;
	case RENTALS_CAR_IN_USE:
		mtmPrintErrorMessage(stream,MTM_CAR_IN_USE);
		break;
	case RENTALS_CAR_DOES_NOT_EXISTS:
		mtmPrintErrorMessage(stream,MTM_CAR_DOES_NOT_EXISTS);
		break;
	case RENTALS_CAR_ALREADY_ON_SALE:
		mtmPrintErrorMessage(stream,MTM_CAR_ALREADY_ON_SALE);
		break;
	case RENTALS_SALE_PRICE_TOO_BIG:
		mtmPrintErrorMessage(stream,MTM_SALE_PRICE_TOO_BIG);
		break;
	case RENTALS_CAR_NO_SALE:
		mtmPrintErrorMessage(stream,MTM_CAR_NO_SALE);
		break;
	case RENTALS_CUSTOMER_ALREADY_EXISTS:
		mtmPrintErrorMessage(stream,MTM_CUSTOMER_ALREADY_EXISTS);
		break;
	case RENTALS_INVALID_CUSTOMER_ID:
		mtmPrintErrorMessage(stream,MTM_INVALID_CUSTOMER_ID);
		break;
	case RENTALS_NO_CARS:
		mtmPrintErrorMessage(stream,MTM_NO_CARS);
		break;
	case RENTALS_NO_SALES:
		mtmPrintErrorMessage(stream,MTM_NO_SALES);
		break;
	case RENTALS_NO_OPEN_BILLS:
		mtmPrintErrorMessage(stream,MTM_NO_OPEN_BILLS);
		break;
	case RENTALS_INVALID_PRICE:
		mtmPrintErrorMessage(stream,MTM_INVALID_PRICE);
		break;
	case RENTALS_INVALID_KM:
		mtmPrintErrorMessage(stream,MTM_INVALID_KM);
		break;
	case RENTALS_INVALID_CAR:
		mtmPrintErrorMessage(stream,MTM_INVALID_CAR);
		break;
	default:
		assert(0);
	}
}
//**********************************************************************
//**                          Car commands                            **
//**********************************************************************

/*
 * Handles the "car add" command.
 * Expects 4 parameters:
 * 1. the license of the car.
 * 2. the name of the car.
 * 3. the price of the car for rent.
 * 4. the kilometrage of the car.
 */
static RentalsResult parseCarAddCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int license;
	char* name;
	int price;
	int km;
	//number of parameters must be 6
	assert(numberOfParameters == 6);

	//third parameter is the license
	license = atoi(parameters[2]);

	//4th parameter is the name
	name = parameters[3];

	//5th parameter is the price
	price = atoi(parameters[4]);

	//6th parameters is the km
	km = atoi(parameters[5]);

	return rentalsAddNewCar(handler->rentals, license, name, price, km);
}

/*
 * Handles the "car remove" command.
 * Expects 1 additional parameter:
 * 1. the license of the car.
 */
static RentalsResult parseCarRemoveCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int license;
	//number of parameters must be 3
	assert(numberOfParameters == 3);

	//third parameter is the license
	license = atoi(parameters[2]);

	return rentalsRemoveCar(handler->rentals, license);
}

/*
 * Handles the "car sale" command.
 * Expects 4 additional parameter:
 * 1. the license of the car.
 * 2. the minimum km of the car in the sale.
 * 3. the maximum km of the car in the sale.
 * 4. the price of the sale.
 */
static RentalsResult parseCarSaleCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int license;
	int minKm;
	int maxKm;
	int price;
	//number of parameters must be 6
	assert(numberOfParameters == 6);

	//third parameter is the license
	license = atoi(parameters[2]);

	//4th parameter is the minKm
	minKm = atoi(parameters[3]);

	//5th parameters is the maxKm
	maxKm = atoi(parameters[4]);

	//6th parameters is the price
	price = atoi(parameters[5]);

	return rentalsPutOnSale(handler->rentals, license, minKm, maxKm, price);
}

/*
 * Handles the "car nosale" command.
 * Expects 1 additional parameter:
 * 1. the license of the car.
 */
static RentalsResult parseCarNosaleCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int license;
	//number of parameters must be 3
	assert(numberOfParameters == 3);

	//third parameter is the license
	license = atoi(parameters[2]);

	return rentalsEndSale(handler->rentals, license);
}

/*
 * Handles the "car" command.
 * Expects one of the following sub commands:
 * 1. add
 * 2. remove
 * 3. sale
 * 4. nosale
 */
static RentalsResult parseCarCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	char* subcommand;
	//number of parameters must be more than 1
	assert(numberOfParameters > 1);

	subcommand = parameters[1];

	if (strcmp(subcommand, SUBCOMMAND_ADD) == ZERO) {
		return parseCarAddCommand(handler, parameters, numberOfParameters);
	} else if (strcmp(subcommand, SUBCOMMAND_REMOVE) == ZERO){
		return parseCarRemoveCommand(handler,parameters,numberOfParameters);
	} else if (strcmp(subcommand, SUBCOMMAND_SALE) == ZERO) {
		return parseCarSaleCommand(handler, parameters, numberOfParameters);
	} else if (strcmp(subcommand, SUBCOMMAND_NO_SALE) == ZERO) {
		return parseCarNosaleCommand(handler, parameters,numberOfParameters);
	}
	return RENTALS_SUCCESS;
}
//**********************************************************************
//**                     Customer commands                            **
//**********************************************************************

/*
 * Handles the "customer new" command.
 * Expects 2 additional parameter:
 * 1. the id of the customer.
 * 2. the name of the customer.
 */
static RentalsResult parseCustomerNewCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int id;
	char* name;
	//number of parameters must be 4
	assert(numberOfParameters == 4);

	//third parameter is the id
	id = atoi(parameters[2]);

	//4th parameter is the name
	name = parameters[3];

	return rentalsAddNewCustomer(handler->rentals, id, name);
}

/*
 * Handles the "customer rent" command.
 * Expects 2 additional parameter:
 * 1. the id of the customer.
 * 2. the license of the car.
 */
static RentalsResult parseCustomerRentCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int id;
	int license;
	//number of parameters must be 4
	assert(numberOfParameters == 4);

	//third parameter is the id
	id = atoi(parameters[2]);

	//4th parameter is the name
	license = atoi(parameters[3]);

	return rentalsCustomerRentCar(handler->rentals, id, license);
}

/*
 * Handles the "customer return" command.
 * Expects 3 additional parameter:
 * 1. the id of the customer.
 * 2. the license of the car.
 * 3. the km of the car.
 */
static RentalsResult parseCustomerReturnCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int id;
	int license;
	int km;
	//number of parameters must be 5
	assert(numberOfParameters == 5);

	//third parameter is the id
	id = atoi(parameters[2]);

	//4th parameter is the name
	license = atoi(parameters[3]);

	//5th parameter is the km
	km = atoi(parameters[4]);

	return rentalsCustomerReturnCar(handler->rentals, id, license, km);
}

/*
 * Handles the "customer pay" command.
 * Expects 1 additional parameter:
 * 1. the id of the customer.
 */
static RentalsResult parseCustomerPayCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int id;
	//number of parameters must be 3
	assert(numberOfParameters == 3);

	//third parameter is the id
	id = atoi(parameters[2]);

	return rentalsCustomerPayBill(handler->rentals, id);
}

/*
 * Handles the "customer" command.
 * Expects one of the following sub commands:
 * 1. new
 * 2. rent
 * 3. return
 * 4. pay
 */
static RentalsResult parseCustomerCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	char* subcommand;
	//number of parameters must be more than 1
	assert(numberOfParameters > 1);

	subcommand = parameters[1];

	if (strcmp(subcommand,SUBCOMMAND_NEW) == ZERO) {
		return parseCustomerNewCommand(handler,parameters,numberOfParameters);
	} else if (strcmp(subcommand,SUBCOMMAND_RENT) == ZERO){
		return parseCustomerRentCommand(handler,parameters,numberOfParameters);
	} else if (strcmp(subcommand,SUBCOMMAND_RETURN) == ZERO) {
		return parseCustomerReturnCommand(handler,parameters,
														   numberOfParameters);
	} else if (strcmp(subcommand,SUBCOMMAND_PAY) == ZERO) {
		return parseCustomerPayCommand(handler, parameters,numberOfParameters);
	}
	return RENTALS_SUCCESS;
}

//**********************************************************************
//**                       Report commands                            **
//**********************************************************************

/*
 * Handles the "report available_car" command.
 * Expects no additional parameter.
 */
static RentalsResult parseReportAvailableCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	//number of parameters must be 2
	assert(numberOfParameters == 2);

	return rentalsPrintAvailableCars(handler->rentals, handler->output);
}

/*
 * Handles the "report bills" command.
 * Expects no additional parameter.
 */
static RentalsResult parseReportBillsCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	//number of parameters must be 2
	assert(numberOfParameters == 2);

	return rentalsPrintOpenBills(handler->rentals, handler->output);
}

/*
 * Handles the "report customer" command.
 * Expects 1 additional parameter:
 * 1. the id of the customer.
 */
static RentalsResult parseReportCustomerCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	int id;
	//number of parameters must be 3
	assert(numberOfParameters == 3);
	//third parameter is the id
	id = atoi(parameters[2]);

	return rentalsPrintCustomerBill(handler->rentals, id, handler->output);
}

/*
 * Handles the "report monthly" command.
 * Expects no additional parameter.
 */
static RentalsResult parseReportMonthlyCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	//number of parameters must be 2
	assert(numberOfParameters == 2);
	return rentalsPrintIncomeSummary(handler->rentals, handler->output);
}

/*
 * Handles the "report" command.
 * Expects one of the following sub commands:
 * 1. available_cars
 * 2. bills
 * 3. customer
 * 4. monthly
 */
static RentalsResult parseReportCommand(InputHandler *handler,
								char* parameters[], int numberOfParameters){
	char* subcommand;
	//number of parameters must be more than 1
	assert(numberOfParameters > 1);
	subcommand = parameters[1];

	if (strcmp(subcommand,SUBCOMMAND_AVAILABLE_CARS) == ZERO) {
		return parseReportAvailableCommand(handler,parameters,
														  numberOfParameters);
	} else if (strcmp(subcommand,SUBCOMMAND_BILLS) == ZERO){
		return parseReportBillsCommand(handler,parameters,numberOfParameters);
	} else if (strcmp(subcommand,SUBCOMMAND_CUSTOMER) == ZERO) {
		return parseReportCustomerCommand(handler,parameters,
														  numberOfParameters);
	} else if (strcmp(subcommand,SUBCOMMAND_MONTHLY) == ZERO) {
		return parseReportMonthlyCommand(handler, parameters,
														  numberOfParameters);
	}
	return RENTALS_SUCCESS;
}

RentalsResult inputHandlerInit(InputHandler *handler, FILE *inputStream,
		FILE *outputStream, FILE *errorStream) {
	handler->rentals = rentalsCreateNew();
	if (handler->rentals == NULL) {
		return RENTALS_OUT_OF_MEMORY;
	}
	handler->input = inputStream;
	handler->output = outputStream;
	handler->error = errorStream;

	return RENTALS_SUCCESS;
}

void inputHandlerDestroy(InputHandler *handler) {
	rentalsDestroy(handler->rentals);
}

/*
 * Handles a single input line.
 * Expects one of the following commands:
 * 1. car
 * 2. customer
 * 3. report
 */
RentalsResult inputHandlerOperateLine(InputHandler *handler, char *line) {
	// the separate parameters from the line
	char* parameters[MAX_WORDS_IN_COMMAND];
	// the number of parameters parsed
	int numberOfParameters;
	// the line command
	char* command;
	// parse the line into tokens
	numberOfParameters = splitLineIntoWords(line, parameters);

	// if this is a comment, simply skip it
	if ((numberOfParameters > 0) && (strncmp(parameters[0], COMMAND_COMMENT,
			sizeof(COMMAND_COMMENT) - 1) == 0)) {
		return RENTALS_SUCCESS;
	}
	// check if the line was empty, in which case we simply skip it
	if (numberOfParameters == 0) {
		return RENTALS_SUCCESS;
	}
	// verify we don't have a problem with the number of parameters
	assert ((numberOfParameters >= 0) &&
			(numberOfParameters < MAX_WORDS_IN_COMMAND));

	// handle the rest of the line according to the command
	command = parameters[0];
	if (strcmp(command, COMMAND_CAR ) == 0) {
		return parseCarCommand(handler, parameters, numberOfParameters);
	} else if (strcmp(command, COMMAND_CUSTOMER) == 0) {
		return parseCustomerCommand(handler, parameters, numberOfParameters);
	} else{
		assert(strcmp(command, COMMAND_REPORT) == 0);
		return parseReportCommand(handler, parameters, numberOfParameters);
	}
	return RENTALS_SUCCESS;
}

RentalsResult inputHandlerHandle(InputHandler *handler) {
	// the line to read into
	char line[MAX_LINE_LENGTH];
	//the return code from the line handled
	RentalsResult handlerResult;
	// successively read lines from the input, and handle each one separately
		while (fgets(line, MAX_LINE_LENGTH, handler->input) != NULL) {
			handlerResult = inputHandlerOperateLine(handler, line);
			if (handlerResult != RENTALS_SUCCESS) {
				// if there was an error, report it or return
				if (handlerResult == RENTALS_OUT_OF_MEMORY) {
					return handlerResult;
				}
				printReturnCode(handler->error, handlerResult);
			}
		}
	return RENTALS_SUCCESS;
}


//**********************************************************************
//**                         Help commands                            **
//**********************************************************************

/*
 * Resolves the given string to a type of file to open.
 * Returns the type of file to open.
 * If the prefix is illegal, return FILE_BAD_PREFIX.
 * @param prefix the prefix string
 */
static int resolveTypeFlag(const char *prefix) {
	// check if the prefix is for an input file
	if (strcmp(prefix, PARAMETER_INPUT_PREFIX) == 0) {
		return FILE_IN;
	}
	// check if the prefix is for an output file
	else if (strcmp(prefix, PARAMETER_OUTPUT_PREFIX) == 0) {
		return FILE_OUT;
	}
	// if none of the above worked, there is a problem with the arguments
	return FILE_BAD_PREFIX;
}

/*
 * Loads the file according to the given type.
 * Returns a result code for success or failure.
 * @param fileType whether load as input or as output
 * @param filename the name of the file to load
 * @param in a pointer to place the opened file in
 * @param out a pointer to place the opened file out
 * @param err the stream into which to print the error
 */
static int openFile(int fileType, const char *filename, FILE **in, FILE **out,
		FILE *err) {
	assert(fileType == FILE_IN || fileType == FILE_OUT);
	if (fileType == FILE_IN) {
		*in = fopen(filename, READ_MODE);
		if (*in == NULL) {
			return LOAD_FAIL;
		}
	} else { // file1Type == FILE_OUT
		*out = fopen(filename, WRITE_MODE);
		if (*out == NULL) {
			return LOAD_FAIL;
		}
	}
	return LOAD_SUCCESS;
}

/*
 * Loads the files from the command line arguments.
 * Returns a result code for success or failure.
 * @param argc the number of parameters
 * @param argv the parameters
 * @param in a pointer to place the opened file in
 * @param out a pointer to place the opened file out
 * @param err the stream into which to print the error
 */
static int loadFiles(int argc, char **argv, FILE **in, FILE **out, FILE *err) {
	int file1Type = FILE_NONE, file2Type = FILE_NONE;
	// check each of the cases for arguments
	switch (argc) {
	case 1: // no arguments
		break;
	case 5: // both PARAMETER_INPUT_PREFIX and PARAMETER_OUTPUT_PREFIX
		file2Type = resolveTypeFlag (*(argv+3));
		// no break, skip to next clause to resolve type of file1
	case 3: // either PARAMETER_INPUT_PREFIX or PARAMETER_OUTPUT_PREFIX
		file1Type = resolveTypeFlag (*(argv+1));
		break;
	default: // invalid amount of arguments
		printReturnCode(err,RENTALS_INVALID_COMMAND_LINE_PARAMETERS);
		return LOAD_FAIL;
	}
	// if one of the file types was illegal print error and exit
	if (file1Type == FILE_BAD_PREFIX || file2Type == FILE_BAD_PREFIX) {
		printReturnCode(err,RENTALS_INVALID_COMMAND_LINE_PARAMETERS);
		return LOAD_FAIL;
	}
	// if none of the files needs to be set, return success
	if (file1Type == FILE_NONE && file2Type == FILE_NONE) {
		return LOAD_SUCCESS;
	}
	// otherwise, we need to load the files
	// check that we didn't get the same type twice
	if (file1Type == file2Type) {
		printReturnCode(err,RENTALS_INVALID_COMMAND_LINE_PARAMETERS);
		return LOAD_FAIL;
	}
	// load the first file, it can't be NONE
	if (openFile(file1Type, argv[2], in, out, err) == LOAD_FAIL) {
		printReturnCode(err,RENTALS_CANNOT_OPEN_FILE);
		return LOAD_FAIL;
	}
	// check whether we need to load the second file
	if (file2Type == FILE_NONE) {
		return LOAD_SUCCESS;
	}
	// otherwise we need to load the second file
	// in case of failure, we need to close the first file
	if (openFile(file2Type, argv[4], in, out, err) == LOAD_FAIL) {
		// check whether we loaded file2. if we did, close it first
		switch (file1Type) {
		case FILE_IN:
			fclose(*in);
			break;
		case FILE_OUT:
			fclose(*out);
			break;
		}
		// if the file close fails, we can do nothing
		// now we can fail the program
		printReturnCode(err,RENTALS_CANNOT_OPEN_FILE);
		return LOAD_FAIL;
	}
	return LOAD_SUCCESS;
}

/*!
 * Our main function, we use stdin for input.
 * The function reads a line from the input as long as it exists.
 * Then it splits the line into words and executes the right command
 *
 * @param argc - unused
 * @param argv - unused
 * @return 0 in case of success
 */
int main(int argc, char **argv) {
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

// set the default streams
	FILE *in = stdin;
	FILE *out = stdout;
	FILE *err = stderr;

// the handler and return code from the handler
	InputHandler handler;
	RentalsResult handlerResult;
	if (loadFiles(argc, argv, &in, &out, err) != LOAD_SUCCESS) {
		return 1;
	}
// run the actual handler
	inputHandlerInit(&handler, in, out, err);
	handlerResult = inputHandlerHandle(&handler);
// check for errors
	if (handlerResult != RENTALS_SUCCESS) {
		// if this is an OUT_OF_MEMORY error, exit immediately
		if (handlerResult == RENTALS_OUT_OF_MEMORY) {
			printReturnCode(handler.error, handlerResult);
			return 0;
		}
	}
	// destroy the handler and close files
	inputHandlerDestroy(&handler);
	if (in != stdin) {
		fclose(in);
	}
	if (out != stdout) {
		fclose(out);
	}
	return 0;
}
